package lumis.portal.localization;

import lumis.portal.*;
import lumis.portal.authentication.SessionConfig;
import lumis.portal.cache.PortalCache;
import lumis.portal.dao.*;
import lumis.portal.manager.*;

import lumis.util.CollectionIterableWrapper;
import lumis.util.IResource;
import lumis.util.ITransaction;
import lumis.util.LocaleUtil;
import lumis.util.PortalUtil;
import lumis.util.TextUtil;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

import java.util.*;
import java.util.regex.*;


/**
 * Class responsible for localizing strings, using static resources from xml files and dynamic resources (custom strings) from database. Each string resource is cached upon first access.
 * 
 * @version $Revision: 9271 $ $Date: 2008-05-14 12:54:20 -0300 (Wed, 14 May 2008) $
 * @since 4.0.0
 */
public class LocalizationManager implements ILocalizationManager
{
	private IResource stringsResource = new ManagerStringResource();
	
	public LocalizationManager()
	{
		stringsResource.addResourcePath("lumis/service/locale/strings/strings");
	}
	/**
	 * Cache of resource tables. Changes to this cache should be done at the same time
	 * of changes to the {@link #resourcePathsTable} in a block synchronized by this manager instance.
	 * @see #resourcePathsCached 
	 * @since 4.0.6
	 */
	private PortalCache<Map<String, String>> resourcePathsTable = new PortalCache<Map<String, String>>("lumis.portal.localization.LocalizationManager.resourcePathsTable");
	
	/**
	 * Keys that have been stored in the cache. These keys may not be in the cache at this time,
	 * since the cache has its own control of invalidation if the capacity was reached. But all
	 * cached entries' keys are in this set. Changes to this Set should be done at the same time
	 * of changes to the cache in a block synchronized by this manager instance.
	 * @see #resourcePathsTable
	 * @since 4.0.6
	 */
	private Set<String> resourcePathsCached = new HashSet<String>();

	/**
	 * @since 4.0.7
	 */
	private static ILogger logger = LoggerFactory.getLogger(LocalizationManager.class);
	
	/**
	 * The locale DAO instance.
	 * @version $Revision: 9271 $ $Date: 2008-05-14 12:54:20 -0300 (Wed, 14 May 2008) $
	 * @since 4.0.10
	 */
	private ILocaleDao localeDao = new LocaleDaoHib();
	
	/**
	 * Returns a collection of locale config
	 * @version $Revision: 9271 $ $Date: 2008-05-14 12:54:20 -0300 (Wed, 14 May 2008) $
	 * @since 4.0.10
	 */
	public Collection<LocaleConfig> getLocales(ITransaction transaction) throws PortalException 
	{
		try
		{
			return localeDao.getLocales(transaction);
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}
	
	/**
	 * If there is a current session config, returns its locale, otherwise 
	 * returns the portal default locale.
	 * @return the locale.
	 * @see SessionConfig#getCurrentSessionConfig()
	 * @see PortalContext#getDefaultLocale()
	 * @since 4.0.10
	 */
	private Locale getCurrentDefaultLocale()
	{
		SessionConfig sessionConfig = SessionConfig.getCurrentSessionConfig();
		Locale locale;
		if (sessionConfig != null)
			locale = sessionConfig.getLocale();
		else
			locale = PortalContext.getDefaultLocale();
		return locale;
	}
	
	public String localize(String id, IResource resource) throws ManagerException, PortalException
	{
		try
		{
			return localize(id, resource, getCurrentDefaultLocale());
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}

	public String localize(String id, List<IResource> resources) throws ManagerException, PortalException
	{
		try
		{
			return localize(id, resources, getCurrentDefaultLocale());
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}

	public String localize(String id, IResource resource, Locale locale) throws ManagerException, PortalException
	{
		try
		{
			if(id == null)
				return null;
			
			if (id.indexOf("[lumis:localize]") > -1)
				return localizeText(id, resource, locale);
			
			String[] argsArray = id.split("(?<!\\\\);", -1); // split by ; not anteceded by a \
		
			// replace \; by ; on the strings
			for (int i=0; i<argsArray.length; i++)
				argsArray[i] = TextUtil.stringReplace(argsArray[i], "\\;", ";");

			String stringId = argsArray[0];

			// try to get the string as parameterized stringId (pattern)
			String patternString = getCachedString(stringId, resource, locale);
			if (patternString != null)
			{
				List<IResource> resources = Arrays.asList(resource);
				return renderString(patternString, argsArray, resources, locale);
			}

			// get the string as not parameterized
			String string = getCachedString(id, resource, locale);
			if (string != null)
				return string;
			else
				return id;
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}

	}

	public String localize(String id, List<IResource> resources, Locale locale) throws ManagerException, PortalException
	{
		try
		{
			if(id == null)
				return null;
			
			if (id.indexOf("[lumis:localize]") > -1)
				return localizeText(id, resources, locale);
			
			String[] argsArray = id.split("(?<!\\\\);", -1); // split by ; not anteceded by a \
			
			// replace \; by ; on the strings
			for (int i=0; i<argsArray.length; i++)
				argsArray[i] = TextUtil.stringReplace(argsArray[i], "\\;", ";");

			String stringId = argsArray[0];

			for (IResource resource : resources)
			{
				// try to get the string as parameterized stringId (pattern)
				String patternString = getCachedString(stringId, resource, locale);
				if (patternString != null)
					return renderString(patternString, argsArray, resources, locale);
	
				// get the string as not parameterized
				String string = getCachedString(id, resource, locale);
				if (string != null)
					return string;
			}
			
			return id;
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}

	}

	private String localizeText(String text, IResource resource, Locale locale) throws ManagerException, PortalException
	{
		try
		{
			Pattern p = Pattern.compile("\\[lumis:localize\\].*?\\[\\/lumis:localize\\]");
			Matcher m = p.matcher(text);
			StringBuffer sb = new StringBuffer();
			String stringId;
			String match;
			while (m.find())
			{
				match = m.group();
				stringId = match.substring(16, match.length() - 17);
				m.appendReplacement(sb, Matcher.quoteReplacement(localize(stringId, resource, locale)));
			}
			m.appendTail(sb);

			String localizedText = sb.toString();
			
			if (localizedText.indexOf("[lumis:localize]") > -1)
				return localizeText(localizedText, resource, locale);
			else
				return localizedText;
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}

	}

	private String localizeText(String text, List<IResource> resources, Locale locale) throws ManagerException, PortalException
	{
		try
		{
			Pattern p = Pattern.compile("\\[lumis:localize\\].*?\\[\\/lumis:localize\\]");
			Matcher m = p.matcher(text);
			StringBuffer sb = new StringBuffer();
			String stringId;
			String match;
			while (m.find())
			{
				match = m.group();
				stringId = match.substring(16, match.length() - 17);
				m.appendReplacement(sb, Matcher.quoteReplacement(localize(stringId, resources, locale)));
			}
			m.appendTail(sb);

			String localizedText = sb.toString();
			
			if (localizedText.indexOf("[lumis:localize]") > -1)
				return localizeText(localizedText, resources, locale);
			else
				return localizedText;
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}

	}

	private String getCachedString(String id, IResource resource, Locale locale) throws ManagerException, PortalException
	{
		if(locale == null)
			locale = getCurrentDefaultLocale();
			
		String localizedString = getCachedStringInternal(id, resource, locale);
		if (localizedString == null)
		{
			localizedString = getCachedStringInternal(id, resource, new Locale(locale.getLanguage()));
			if (localizedString == null && !locale.equals(PortalContext.getDefaultLocale())) 
			{
				locale = PortalContext.getDefaultLocale();
				localizedString = getCachedStringInternal(id, resource, locale);
				if (localizedString == null)
					localizedString = getCachedStringInternal(id, resource, new Locale(locale.getLanguage()));
			}
		}
		
		return localizedString;
	}
	
	private String getCachedStringInternal(String id, IResource resource, Locale locale) throws ManagerException, PortalException
	{
		try
		{
			String resourcePath;
			String localizedString;
			String key;
			Map<String, String> stringTable;

			for (Enumeration<String> e = resource.getResourcePaths(); e.hasMoreElements();)
			{
				resourcePath = e.nextElement();
				key = resourcePath + "." + locale.toString();
				stringTable = resourcePathsTable.get(key);
				if (stringTable != null)
				{
					localizedString = stringTable.get(id);
					if (localizedString != null)
						return localizedString;
				}
				else
				{
					synchronized (this)
					{
						// must check for string table within synchronized block
						// again
						stringTable = resourcePathsTable.get(key);
						if (stringTable != null)
						{
							localizedString = stringTable.get(id);
							if (localizedString != null)
								return localizedString;
						}
						else
						{
							try
							{
								stringTable = DaoFactory.getMainStringLocalizationDao().getStringTable(resourcePath, locale);
								
								// In order to avoid infinite loop while trying to localize exception messages, the code below never can throw a Portal Exception
								stringTable.putAll(DaoFactory.getCustomStringLocalizationDao().getStringTable(resourcePath, locale));
							}
							catch (PortalException e1)
							{
								logger.error("Invalid resource: " + resourcePath + "." + locale.toString(), e1);
								continue;
							}
							resourcePathsTable.put(key, Collections.unmodifiableMap(stringTable));
							resourcePathsCached.add(key);

							localizedString = stringTable.get(id);
							if (localizedString != null)
								return localizedString;
						}
					}
				}
			}

			return null;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}

	}

	private String renderString(String stringPattern, String[] argsArray, List<IResource> resources, Locale locale) throws ManagerException, PortalException
	{
		try
		{
			if (argsArray.length == 2 && argsArray[1].equals(ILocalizationManager.DO_NOT_RENDER_PARAMETERS))
				return stringPattern;
			for (int i = argsArray.length-1; i >=1; i--)
				stringPattern = stringPattern.replaceAll("%" + i, TextUtil.escapeRegexpReplacement(localize(argsArray[i], resources, locale)));
			stringPattern = stringPattern.replaceAll("%\\d+(?=[^\\d]*?)", "(?)");

			return stringPattern;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}

	public synchronized void clearCache() throws ManagerException, PortalException
	{
		resourcePathsTable.clear();
		resourcePathsCached.clear();
	}

	public synchronized void clearCache(String resourcePath) throws ManagerException, PortalException
	{
		for (Iterator i = resourcePathsCached.iterator(); i.hasNext();)
		{
			String key = (String) i.next();
			if (key.indexOf(resourcePath + ".") == 0)
			{
				i.remove();
				resourcePathsTable.remove(key);
			}
		}
	}

	public synchronized void clearCache(String resourcePath, Locale locale) throws ManagerException, PortalException
	{
		String key = resourcePath + "." + locale.toString();
		resourcePathsTable.remove(key);
		resourcePathsCached.remove(key);
	}
	
	public Iterable<String> getCachedResourcePaths() throws ManagerException
	{
		return new CollectionIterableWrapper<String>(resourcePathsCached);
	}
	
	public String addCustomString (SessionConfig sessionConfig, CustomStringConfig customStringConfig, ITransaction transaction) throws ManagerException, PortalException
	{
		String customStringId = DaoFactory.getCustomStringLocalizationDao().getIdByStringInfo(customStringConfig, transaction);
		
		if (customStringId != null)
			throw new ManagerException("STR_CUSTOM_STRING_ALREADY_EXIST", stringsResource);
			
		if (customStringConfig.getCustomStringId() == null)
			customStringConfig.setCustomStringId(PortalUtil.generateNewGuid());
		
		DaoFactory.getCustomStringLocalizationDao().addOrUpdate(customStringConfig, transaction);
		
		Locale locale = null; 
		if (customStringConfig.getLocaleCode() != null)
			locale = LocaleUtil.FromString(customStringConfig.getLocaleCode());
		
		ManagerFactory.getLocalizationManager().clearCache(customStringConfig.getResourcePath(), locale);

		return customStringConfig.getCustomStringId();
	}
	
	public void updateCustomString (SessionConfig sessionConfig, CustomStringConfig customStringConfig, ITransaction transaction) throws ManagerException, PortalException
	{
		String customStringId = DaoFactory.getCustomStringLocalizationDao().getIdByStringInfo(customStringConfig, transaction);

		if (customStringId != null && !customStringConfig.getCustomStringId().equals(customStringId))
			throw new ManagerException("STR_CUSTOM_STRING_ALREADY_EXIST", stringsResource);
		
		DaoFactory.getCustomStringLocalizationDao().addOrUpdate(customStringConfig, transaction);
		
		Locale locale = null; 
		if (customStringConfig.getLocaleCode() != null)
			locale = LocaleUtil.FromString(customStringConfig.getLocaleCode());
		
		ManagerFactory.getLocalizationManager().clearCache(customStringConfig.getResourcePath(), locale);
	}
	
	public void deleteCustomString (SessionConfig sessionConfig, String customStringId, ITransaction transaction) throws ManagerException, PortalException
	{
		CustomStringConfig customStringConfig = DaoFactory.getCustomStringLocalizationDao().get(customStringId, transaction);
		
		DaoFactory.getCustomStringLocalizationDao().delete(customStringId, transaction);
		
		Locale locale = null; 
		if (customStringConfig.getLocaleCode() != null)
			locale = LocaleUtil.FromString(customStringConfig.getLocaleCode());
		
		ManagerFactory.getLocalizationManager().clearCache(customStringConfig.getResourcePath(), locale);
	}

	public CustomStringConfig getCustomString(SessionConfig sessionConfig, String customStringId, ITransaction transaction) throws PortalException
	{
		return DaoFactory.getCustomStringLocalizationDao().get(customStringId, transaction);
	}
}
